using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System;
using System.Reflection;

namespace Polymod
{
    internal class FormulaVisitor : ExpressionVisitor
    {
        private ParameterExpression _target = Expression.Parameter(typeof(IModel), "p0");
        private bool _isFirstVisit = true;
        private ParameterExpression _parameter;


        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            IFormula formula;
            FormulaConversionType conversionType;
            if (IsFormula(node, out formula, out conversionType))
            {
                //return CallGetOnModel(formula.Name, formula.Expression.ReturnType);
                //NOTE: Must return same type as node.Type! Handles conversions from int to double etc...
                return CallGetOnModel(formula, node.Type, conversionType);
            }
            else
            {
                return base.VisitMethodCall(node);
            }
        }

        public override Expression Visit(Expression node)
        {
            if (_isFirstVisit)
            {
                var lambdaExpression = node as LambdaExpression;
                if (lambdaExpression == null) throw new InvalidOperationException("Expected a Lambda Expression.");
                //Lambda can only contain a single parameter.
                _parameter = lambdaExpression.Parameters.Single();
                _isFirstVisit = false;
            }
            return base.Visit(node);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (node == _parameter)
            {
                var getMethod = typeof(IModel).GetMethod("GetValue");
                var callGetValueExp = Expression.Call(_target, getMethod);
                var castExp = Expression.Convert(callGetValueExp, node.Type);

                return castExp;
            }
            else
            {
                return base.VisitParameter(node);
            }
        }

        private Expression CallGetOnModel(IFormula formula, Type returnType, FormulaConversionType formulaConversionType)
        {
            if (formulaConversionType == FormulaConversionType.Evaluate)
            {
                var getMethod = typeof(IModel).GetMethod("Get");
                var callGetExp = Expression.Call(_target, getMethod, Expression.Constant(formula.Name));
                var castExp = Expression.Convert(callGetExp, returnType);
                return castExp;
            }

            if (formulaConversionType == FormulaConversionType.AsFunc)
            {
                //TODO: This works absolutely fine, but should I be using a call "Get" here (use the model rather than the underlying object?)
                //_target must be re-thought of if using Get...
                var constant = Expression.Constant(formula.Expression.Compile(), returnType);
                return constant;
            }

            throw new InvalidOperationException();
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            if (node.Parameters.Count == 1 && node.Parameters[0] == _parameter)
            {
                var body = this.Visit(node.Body);
                //NOTE: Convert to an object to suit Func<IModel, OBJECT>...
                var conversion = body.Type.IsClass ? body : Expression.Convert(body, typeof(object));
                return Expression.Lambda(typeof(Func<IModel, object>), conversion, _target);
            }
            else
            {
                return base.VisitLambda(node);
            }
        }

        internal static bool IsFormula(MethodCallExpression node, out IFormula formula, out FormulaConversionType conversionType)
        {
            conversionType = FormulaConversionType.Evaluate;
            formula = null;

            if (!((Enum.TryParse(node.Method.Name, out conversionType))
                && node.Method.DeclaringType.IsGenericType
                && node.Method.DeclaringType.GetGenericTypeDefinition() == typeof(Formula<,>)))
            {
                return false;
            }

            var memberExpression = node.Object as MemberExpression;
            if (memberExpression == null) return false;
            if (memberExpression.NodeType != ExpressionType.MemberAccess) return false;
            var constantExpression = memberExpression.Expression as ConstantExpression;
            if (constantExpression == null) return false;
            formula = (IFormula)((FieldInfo)memberExpression.Member).GetValue(constantExpression.Value);
            return true;
        }
    }
}
